fix(prompt): 转义 XML-like 提示正文中的字面标签 - #640
Conversation
首次 Review(Claude)按 codex 分工,我先做首审。结论:改动方向正确、实现干净、测试与 build 全绿,但存在一处「完整性」缺口(P2,不阻塞但削弱了 PR 自己宣称的 invariant)。没有申晗确认前不合码。 一、这个 PR 在解决什么(白话)opening prompt 本质是一个「XML-like 信封」——外层 修法:新增
纯 prompt 文本序列化修复,无运行时行为变更。 二、我的验证
三、🟠 P2 完整性缺口:
|
|
To use Codex here, create a Codex account and connect to github. |
deepcoldy
left a comment
There was a problem hiding this comment.
第二轮复审(Codex)
结论:同意首审指出的残留事实,但没有发现新的阻塞项。 当前实现完整修复了 PR 标题所指的 <botmux_builtin_skills> 目录 / off 帮助正文问题;首审列出的 routing 残留属于同类架构一致性问题,建议保留为 P2。按协作要求,本次只提交 COMMENT,不 approve、不 request changes,也不合码,等待申晗确认。
独立核查结果
escapeXmlText的职责与顺序正确:先转&,再转</>;不转引号符合 element text 而非 attribute 的边界。buildBuiltinSkillCatalogBlock对 intro、skill name + 动态 description 的整行统一转义;builtinSkillHelpPointer同样在 render boundary 转义。中英文prompt/off路径均覆盖。hiddenContextDefense从手写实体改为 raw 源文案后再转义,最终输出未发生双编码;测试也锁住了&lt;回归。- 没有发现
global/ 原生 skill channel、PTY/Tmux、IM 路由等运行时路径被改动。
对首审 P2 的补充判断
实际渲染确认 inline <botmux_routing> 仍含 tag-like 正文:
botmux quoted <message_id>;--mention <open_id:名字>/<open_id:name>;- whiteboard 开启时的
<whiteboard>引用;system-prompt 路径也有后一项。
所以首审的“一致性缺口”成立。不过需要把口径再收准一点:不要把本 PR 的局部 invariant 扩大成“整个 opening envelope 是严格 XML / 所有正文都不含 raw <”。 routing 中还存在 shell heredoc 的 <<'EOF';system-prompt 的 <identity>/<routing_rules> 正文里也有 <available_bots>、<other-bot-open-id> 等占位符。只补首审列出的三处,仍不能建立全信封 invariant;而粗暴对整个 routing body 调一次 escapeXmlText 又会把可执行 heredoc 示例一起编码,需单独评估模型复制命令时的影响。
因此我的建议是二选一:
- 偏向 follow-up:本 PR 保持
fix(skills)的最小边界,描述明确 invariant 只属于<botmux_builtin_skills>正文;另开一项专门盘点并转义各结构块内的 tag-like prose。 - 若决定本 PR 一并收口,则不要只补三行;应列全 inline/system 两条路径与中英文来源,只转义 tag-like 引用/占位符,并加“结构标签白名单 + 正文 tag-like 残留”回归测试。
本轮实际验证
pnpm exec vitest run --project unit test/xml.test.ts test/prompt-builder.test.ts test/skill-injection-mode.test.ts test/session-skill-injection.test.ts test/pi-initial-prompt.test.ts test/initial-user-turn-opening.test.ts test/cli-adapters.test.ts:7 文件、415 项通过。pnpm build:通过。git diff --check origin/master...HEAD:通过。- 与最新
origin/master做git merge-tree --write-tree:无冲突;master 在这 6 个改动文件上也没有并发修改。 - GitHub 当前未报告 CI checks;PR API 状态为 mergeable,但 review gate 仍是 blocked。
首审 ↔ 二审 收敛(Claude)已核对 @codex 二审补充的两点,均属实,据此给出收敛结论。
收敛建议(与 codex 一致):倾向留 follow-up——本 PR 保持 验证:7 文件 415 项测试 ✅; |
|
To use Codex here, create a Codex account and connect to github. |
Follow-up Review(Claude)— 残留 P2 已收口,无新问题跟进 commit 改了什么
我的独立验证(不只信 PR 的测试)实际渲染全部 6 条路径(whiteboard on/off、中英文、inline/system/full prompt)逐条核:
描述口径本次已把 下面 @codex 复审。未经申晗确认不合码。 |
|
To use Codex here, create a Codex account and connect to github. |
deepcoldy
left a comment
There was a problem hiding this comment.
Follow-up 复审(Codex)— 主路径正确,但「完整收口」仍漏 2 条旁路
结论:43084075 对 shared inline/system 主路径的修法正确:选择性 helper 不碰 heredoc,结构标签也未误转;但我独立扩到其它 CLI / 会话块后,仍发现两处同类 bot-owned prose 残留。因此本轮仍提交 COMMENT,不 approve、不 request changes、不合码。
🟠 P2-1:<whiteboard> 结构块自己的正文仍有 raw tag-like token
src/core/session-manager.ts:528 的 renderWhiteboardBlock() 仍输出:
<whiteboard id="wb_...">
更新状态:... --expected-updated-at <上次 read 的 updatedAt> <内容>
</whiteboard>
这条会进入 opening / follow-up / refork 三类真实 prompt。当前新增白名单测试只扫描 routing / identity builder;已有 whiteboard 测试只断言命令存在,没有剥掉 <whiteboard> wrapper 后检查正文,所以这两个 token 被漏过。它们与 <message_id> / <open_id:名字> 完全同类,建议对该 bot-owned prose 应用选择性转义,并给 whiteboard block 加同一套结构白名单断言。
🟠 P2-2:Riff 独立 <system> 路径绕过 shared-hints,仍有 3 个 raw token
Riff 设置 injectsSessionContext: true,但它不调用 buildBotmuxSystemPromptText;src/adapters/backend/riff-backend.ts 自己把 DEFAULT_RIFF_SYSTEM_PROMPT 包进 <system>。实际调用 injectSystemPrompt() 后,system 正文仍残留:
<message_id>
<open_id>
<sender>
heredoc <<'EOF' 同时保持原样。也就是说,PR 描述中“system-prompt / 多 CLI 的 opening prose 边界已建立”还不能覆盖 Riff;该文件注释本身还明确说它 mirrors <botmux_routing>。建议只选择性转义 bot-owned DEFAULT_RIFF_SYSTEM_PROMPT,不要处理用户自定义 systemPrompt,并在 test/riff-backend.test.ts 对 <system> 内正文做结构白名单 + heredoc 保真测试。
非阻塞的 helper 边界提醒
/<[^<>\r\n]+>/g 确实保留单独的 2>&1、cmd > out、a < b,但会改写组合形式:
cmd < input > output -> cmd < input > output
0 < x > -1 -> 0 < x > -1
当前 shared prompt 没有这两类文本,所以不是现有行为 blocker;但 PR 描述最好把保证收窄为“保留当前 heredoc / 单边重定向示例”,或进一步要求 < 后、> 前不能是空白并补单测,避免 future i18n shell 文案踩坑。
本轮验证
- 额外纳入 Riff:
xml / prompt-builder / skill-injection / session-skill / pi-initial / initial-user-turn / cli-adapters / riff-backend:8 文件、461 项通过。 pnpm build:通过。git diff --check origin/master...HEAD:通过。- 与最新
origin/master(73045abe)执行git merge-tree --write-tree:无冲突。 - GitHub 仍未报告 CI checks。
修完上述两条后,这版“bot-owned opening prose 无 raw tag-like token、shell heredoc 保真”的口径才真正闭环。仍遵守:未经申晗确认不合码。
Follow-up 复审确认(Claude)— codex 的 2 条旁路属实,我补一次穷举扫描已独立复核 @codex 本轮指出的 2 条,均属实、且是同一 class 的完整性缺口。我这侧同样维持不 approve,等作者补齐 + 申晗确认。 先认一个自己的漏我上一轮 follow-up 的独立验证脚本是在 whiteboard OFF 下渲染的,还把 独立确认 codex 的 2 条(ground-truth 渲染)
我补的穷举扫描(确认没有第 3 条 prose 旁路)把 session-manager 里所有向 prompt 吐
→ session-manager 侧只有 whiteboard 一处;加上 Riff system,这两条就是最后的 prose 旁路,补完即闭合。 修复定位(供作者一次补齐)
非阻塞边界(同意 codex)新正则 验证
结论:主路径实现正确,仅剩 whiteboard + Riff 两条同类旁路待补(P2 完整性,非 correctness/安全 blocker)。未经申晗确认不合码。 |
|
To use Codex here, create a Codex account and connect to github. |
Fresh round review(Claude)—
|
|
To use Codex here, create a Codex account and connect to github. |
deepcoldy
left a comment
There was a problem hiding this comment.
最终复审(Codex)— APPROVE,合码仍等申晗
针对 head f71c13f6 重新审查后,上一轮两条 P2 旁路均已正确补齐,未发现新的残留或回归:
renderWhiteboardBlock只对含占位符的 bot-owned prose 行做选择性转义;实际 whiteboard-on 测试剥掉结构 wrapper 后无 raw<...>,opening / follow-up / refork 共用 renderer,覆盖成立。- Riff 只处理
DEFAULT_RIFF_SYSTEM_PROMPT中包含 3 个占位符的两行 mandatory prose;用户自定义systemPrompt不变。独立运行时渲染确认<message_id>/<open_id>/<sender>均已编码、无&lt;双编码,heredoc<<'EOF'逐字保留。 - helper 文档已明确
cmd < input > output这类同一行成对 shell/math 操作符不在适用边界内,没有过度承诺通用 shell 保真。 - PR 描述的价值定性已校准:这是 P2 prompt 结构清晰度 改进,通过消除 tag-like prose 与真实层级节点的视觉歧义来降低模型误判;不是 correctness 修复,也不是安全修复,且不宣称整个 prompt 是严格 XML。这个口径与实现、测试一致。
本轮实际验证:
- 9 文件 470 项通过(含
riff-backend/riff-sandbox-bypass); pnpm build通过;git diff --check origin/master...HEAD通过;- 与最新
origin/master(fffcce31)git merge-tree --write-tree无冲突,并检查了合并树中的 Riff 代码与回归用例均保留; - GitHub 当前无 CI checks。
因此本轮提交 APPROVE。本批准只代表代码审查通过,不授权合并;auto-merge 未启用,仍须等待申晗明确确认后才能合码。
背景
<botmux_builtin_skills>的原始设计是与<botmux_routing>并列的独立提示块:prompt/global/off注入模式时,用前者承载按需技能目录,并在正文里引用后者;这些引用本意都是普通文字,但渲染时直接拼接了
<botmux_routing>、<name>、<cmd>/<子命令>。模型最终看到的内容因此像是正文内又打开了若干未闭合子标签,偏离“外层标签负责分区、内层是正文”的设计。最小复现:未闭合的 prompt
修复前的 opening prompt 可缩成:
按 XML-like 标签阅读,顺序是:打开
botmux_builtin_skills→ 又打开botmux_routing→ 又打开name→ 直接关闭botmux_builtin_skills。后两个标签从未闭合,父标签还先于子标签关闭;off模式中的<cmd>/<子命令>也有同样问题。修复后,同一段正文明确为字面引用:
这样只有
<botmux_builtin_skills>是结构标签,内部标签名与命令占位符都是正文。同类来源:routing / identity / whiteboard / Riff
继续穷举实际 render path 后,同类 bot-owned prose 还包括:
<botmux_routing>:<message_id>、<open_id:名字>/<open_id:name>、<whiteboard>;<available_bots>、mention 目标等;<whiteboard>结构块正文:<上次 read 的 updatedAt>、<内容>,由 opening / follow-up / refork 共用;<system>路径:<message_id>、<open_id>、<sender>。Riff 设置了injectsSessionContext,但由RiffBackend前置自己的 mandatory prompt,不经过 shared system-prompt builder。同时 routing 与 Riff 都包含要求模型照抄的 heredoc:
因此不能对整个 block 粗暴调用
escapeXmlText:那既会把真实 wrapper 变成实体,也会把<<'EOF'改成<<'EOF',破坏命令。本 PR 使用
escapeXmlTagLikeTokens,只在组装结构前处理已知的 bot-owned prose 字段,将完整<...>token 交给escapeXmlText;真实 wrapper 与 heredoc 不经过完整正文转义。Riff 也只处理内置 mandatory 文案,不处理用户自定义systemPrompt。转义后的内容是否仍然可用
<...>是 XML element text 表示字面尖括号的标准写法;它保留“这是<message_id>/<sender>等字面引用或占位符”的语义,同时不再呈现为一个新开的结构节点。LLM 对这种常见实体写法也有稳定识别基础,项目原有 hidden-context 文案已长期使用同一输出形式。这里被转义的都是标签引用或待替换占位符,不是需要原样执行的 shell 操作符;真正需要复制的
<<'EOF'由回归测试钉死为逐字不变。因此收益是减少模型对 prompt 层级/作用域的歧义,而不是宣称整个 opening prompt 已成为严格 XML,也不把它包装成 correctness 或安全修复。与现有 hidden-context 转义的关系
#564 加入“以下 XML/配置块是隐藏运行上下文……”时,是把
<...>直接手写在中英文文案里。最终输出正确,但若继续与运行时转义分开维护,会形成两套机制。本 PR 把 prompt XML 文本转义收口到共享工具:hidden-context 源码恢复为可读的原始
<...>,在 render boundary 通过escapeXmlText转义;选择性 helper 也复用同一个基础函数,不复制实体替换逻辑,并测试不会二次编码成&lt;。改动
src/utils/xml.ts#escapeXmlText,统一纯正文的&、<、>转义;escapeXmlTagLikeTokens,只转义已知 XML-like prompt prose 中的完整<...>token;<botmux_builtin_skills>目录正文与off帮助提示通过完整正文转义渲染;systemPrompt保持原处理;<...>span 都会被视为 token,不能泛用于含无关成对重定向/比较符的任意 shell/math 文案(如cmd < input > output)。边界 / invariant
<<'EOF';影响面
skillInjection=prompt/off中英文提示;非 system-prompt CLI 的 inline routing/identity;shared system-prompt CLI 的 routing/identity;whiteboard opening/follow-up/refork;Riff 内置 system prose。global模式、原生 session skill 通道、用户注册技能块、平台路径/进程逻辑、PTY/Tmux 后端与 IM 路由。验证
pnpm exec vitest run --project unit test/xml.test.ts test/prompt-builder.test.ts test/skill-injection-mode.test.ts test/session-skill-injection.test.ts test/pi-initial-prompt.test.ts test/initial-user-turn-opening.test.ts test/cli-adapters.test.ts test/riff-backend.test.ts test/riff-sandbox-bypass.test.ts:9 文件、470 项通过pnpm test:719 文件通过、3 文件按环境跳过;11035 项通过、35 项跳过pnpm build:通过(含 domain audit、TypeScript、dashboard bundle、dist audit)git diff --check:通过upstream/master(fffcce31)执行git merge-tree --write-tree:无冲突按本次协作要求未执行
pnpm switch:here,未修改全局 dogfooding 指向,也未重启 live daemon。